﻿interface IIoCCBuilder {
	singleInstance(): IIoCCBuilder;
	requires(...dependencies: string[]): IIoCCBuilder;
	dispose(callback: () => void): IIoCCBuilder;
}
interface IIoCC {
	(key: string, factory: IIoCCFactory<any>): IIoCCBuilder;
	(key: string, value: any): IIoCCBuilder;
	(key: string): any;
	has(key: string): boolean;
	clear(): void;
	dispose(): void;
}

interface IIoCCFactory<T> {
	(): T;
}

module IoCC {
	"use strict";

	function has(container: Object, key: string): boolean {
		return Object.prototype.hasOwnProperty.call(container, key);
	}

	function get(container: Object, key: string): IIoCCFactory<any> {
		if (!has(container, key)) {
			throw "The key '" + key + "' is not registered.";
		}
		return container[key];
	}

	function set<T>(container: Object, key: string, factory: IIoCCFactory<T>, allowOverride: boolean = true): void {
		if (has(container, key) && !allowOverride) {
			throw "The key '" + key + "' is already registered.";
		}
		container[key] = factory;
	}

	function remove(container: Object, disposeCallbacks: Object, key: string): void {
		if (!has(container, key)) {
			throw "The key '" + key + "' is not registered.";
		}
		delete container[key];
		if (has(disposeCallbacks, key)) {
			delete disposeCallbacks[key];
		}
	}

	function makeSingleton(container: Object, key: string): void {
		var factory: IIoCCFactory<any> = get(container, key),
			created: boolean = false,
			result: any;

		set(container, key, (): any => {
			if (!created) {
				result = factory.apply(null, arguments);
				created = true;
			}
			return result;
		});
	}

	function makeDependent(container: Object, key: string, dependencies: string[]): void {
		var factory: IIoCCFactory<any> = get(container, key);
		set(container, key, (): any => {
			var dependencyValues: any[] = [],
				dependency: IIoCCFactory<any>,
				i: number;

			for (i = 0; i < dependencies.length; i++) {
				dependency = has(container, dependencies[i]) ? get(container, dependencies[i]) : null;
				if (!dependency) {
					throw "Error wuile building '" + key + "': can't resolve the dependency '" + dependencies[i] + "'.";
				}
				dependencyValues.push(dependency());
			}

			return factory.apply(null, dependencyValues);
		});
	}

	function addDisposeCallback(disposeCallbaks: any, key: string, callback: () => void): void {
		disposeCallbaks[key] = callback;
	}

	function wrap<T>(value: T): IIoCCFactory<T> {
		return (): T => {
			return value;
		};
	}

	function unwrap<T>(value: IIoCCFactory<T>): T {
		return value();
	}

	class DependencyBuilder implements IIoCCBuilder {
		singleInstance: () => IIoCCBuilder;

		requires: (...dependencies: string[]) => IIoCCBuilder;

		dispose: (callback: () => void) => IIoCCBuilder;

		constructor(container: Object, disposeCallbacks: Object, key: string) {
			this.singleInstance = (): IIoCCBuilder => {
				makeSingleton(container, key);
				return this;
			};
			this.requires = (...dependencies: string[]): IIoCCBuilder => {
				makeDependent(container, key, dependencies);
				return this;
			};
			this.dispose = (callback: () => void): IIoCCBuilder => {
				addDisposeCallback(disposeCallbacks, key, callback);
				return this;
			};
		}
	}

	export function makeContainer(): IIoCC {

		var factories: Object = {},
			disposeCallbacks: Object = {};

		function container(key: string, factory?: any): IIoCCBuilder {
			if (arguments.length >= 2) {
				if (typeof factory === "function") {
					set(factories, key, factory, false);
				} else if (factory === null) {
					remove(factories, disposeCallbacks, key);
				} else {
					set(factories, key, wrap<any>(factory), false);
				}
				return new DependencyBuilder(factories, disposeCallbacks, key);
			} else {
				return unwrap<any>(get(factories, key));
			}
		}
		(<IIoCC>container).clear = (): void => {
			factories = {};
			disposeCallbacks = {};
		};
		(<IIoCC>container).has = (key: string): boolean => {
			return has(factories, key);
		};
		(<IIoCC>container).dispose = (): void => {
			var property: string;
			for (property in disposeCallbacks) {
				if (Object.prototype.hasOwnProperty.call(disposeCallbacks, property)) {
					disposeCallbacks[property]();
					disposeCallbacks[property] = null;
				}
			}
			factories = null;
			disposeCallbacks = null;
		};

		return <IIoCC>container;
	}
}

var iocc: IIoCC = IoCC.makeContainer();